<template>
    <div
        ref="reference"
        v-clickoutside="() => toggleDropDownVisible(false)"
        :class="[
            'zx-cascader',
            realSize && `zx-cascader--${realSize}`,
            { 'is-disabled': isDisabled }
        ]"
        @mouseenter="inputHover = true"
        @mouseleave="inputHover = false"
        @click="() => toggleDropDownVisible(readonly ? undefined : true)"
        @keydown="handleKeyDown"
    >
        <ZxInput
            ref="input"
            v-model="multiple ? presentText : inputValue"
            :size="realSize"
            :placeholder="placeholder"
            :readonly="readonly"
            :disabled="isDisabled"
            :validate-event="false"
            :class="{ 'is-focus': dropDownVisible }"
            @focus="handleFocus"
            @blur="handleBlur"
            @input="handleInput"
        >
            <template slot="suffix">
                <i
                    v-if="clearBtnVisible"
                    key="clear"
                    class="zx-input__icon zx-icon-close-circle-fill"
                    style="fontSize:14px"
                    @click.stop="handleClear"
                />
                <i
                    v-else
                    key="arrow-down"
                    style="fontSize:14px"
                    :class="[
                        'zx-input__icon',
                        'zx-icon-down',
                        dropDownVisible && 'is-reverse'
                    ]"
                    @click.stop="toggleDropDownVisible()"
                />
            </template>
        </ZxInput>

        <div v-if="multiple" class="zx-cascader__tags">
            <ZxTag
                v-for="(tag, index) in presentTags"
                :key="tag.key"
                type="info"
                :size="tagSize"
                :hit="tag.hitState"
                :closable="tag.closable"
                disable-transitions
                @close="deleteTag(index)"
            >
                <span>{{ tag.text }}</span>
            </ZxTag>
            <input
                v-if="filterable && !isDisabled"
                v-model.trim="inputValue"
                type="text"
                class="zx-cascader__search-input"
                :placeholder="presentTags.length ? '' : placeholder"
                @input="e => handleInput(inputValue, e)"
                @click.stop="toggleDropDownVisible(true)"
                @keydown.delete="handleDelete"
            >
        </div>

        <Transition name="zx-zoom-in-top" @after-leave="handleDropdownLeave">
            <div
                v-show="dropDownVisible"
                ref="popper"
                :class="['zx-popper', 'zx-cascader__dropdown', popperClass]"
            >
                <ZxCascaderPanel
                    v-show="!filtering"
                    ref="panel"
                    v-model="checkedValue"
                    :options="options"
                    :props="config"
                    :border="false"
                    :render-label="$scopedSlots.default"
                    @expand-change="handleExpandChange"
                    @close="toggleDropDownVisible(false)"
                />
                <ZxScrollbar
                    v-if="filterable"
                    v-show="filtering"
                    ref="suggestionPanel"
                    tag="ul"
                    class="zx-cascader__suggestion-panel"
                    view-class="zx-cascader__suggestion-list"
                    @keydown.native="handleSuggestionKeyDown"
                >
                    <template v-if="suggestions.length">
                        <li
                            v-for="(item, index) in suggestions"
                            :key="item.uid"
                            :class="[
                                'zx-cascader__suggestion-item',
                                item.checked && 'is-checked'
                            ]"
                            :tabindex="-1"
                            @click="handleSuggestionClick(index)"
                        >
                            <span>{{ item.text }}</span>
                            <i v-if="item.checked" style="fontSize:14px" class="zx-icon-check" />
                        </li>
                    </template>
                    <slot v-else name="empty">
                        <li class="zx-cascader__empty-text">{{ t('el.cascader.noMatch') }}</li>
                    </slot>
                </ZxScrollbar>
            </div>
        </Transition>
    </div>
</template>

<script>
import Popper from '@src/utils/vue-popper';
import Clickoutside from '@src/utils/clickoutside';
import Emitter from '@src/mixins/emitter';
import Locale from '@src/mixins/locale';
import Migrating from '@src/mixins/migrating';
import ZxInput from '../../../components/input';
import ZxTag from '../../../components/tag';
import ZxScrollbar from '../../../components/scrollbar';
import ZxCascaderPanel from '../../../components/cascader-panel';
import AriaUtils from '@src/utils/aria-utils';
import { t } from '@src/locale';
import { isEqual, isEmpty, kebabCase } from '@src/utils/util';
import { isUndefined, isFunction } from '@src/utils/types';
import { isDef } from '@src/utils/shared';
import { addResizeListener, removeResizeListener } from '@src/utils/resize-event';
import debounce from 'throttle-debounce/debounce';

const { keys: KeyCode } = AriaUtils;
const MigratingProps = {
    expandTrigger: {
        newProp: 'expandTrigger',
        type: String,
    },
    changeOnSelect: {
        newProp: 'checkStrictly',
        type: Boolean,
    },
    hoverThreshold: {
        newProp: 'hoverThreshold',
        type: Number,
    },
};

const PopperMixin = {
    props: {
        placement: {
            type: String,
            default: 'bottom-start',
        },
        appendToBody: Popper.props.appendToBody,
        visibleArrow: {
            type: Boolean,
            default: true,
        },
        arrowOffset: Popper.props.arrowOffset,
        offset: Popper.props.offset,
        boundariesPadding: Popper.props.boundariesPadding,
        popperOptions: Popper.props.popperOptions,
    },
    methods: Popper.methods,
    data: Popper.data,
    beforeDestroy: Popper.beforeDestroy,
};

const InputSizeMap = {
    medium: 36,
    small: 32,
    mini: 28,
};

export default {
    name: 'ZxCascader',

    directives: { Clickoutside },

    components: {
        ZxInput,
        ZxTag,
        ZxScrollbar,
        ZxCascaderPanel,
    },

    mixins: [PopperMixin, Emitter, Locale, Migrating],

    inject: {
        zxForm: {
            default: '',
        },
        zxFormItem: {
            default: '',
        },
    },

    props: {
        value: {},
        options: Array,
        props: Object,
        size: String,
        placeholder: {
            type: String,
            default: () => t('el.cascader.placeholder'),
        },
        disabled: Boolean,
        clearable: Boolean,
        filterable: Boolean,
        filterMethod: Function,
        separator: {
            type: String,
            default: ' / ',
        },
        showAllLevels: {
            type: Boolean,
            default: true,
        },
        collapseTags: Boolean,
        debounce: {
            type: Number,
            default: 300,
        },
        beforeFilter: {
            type: Function,
            default: () => () => {},
        },
        popperClass: String,
    },

    data() {
        return {
            dropDownVisible: false,
            checkedValue: this.value || null,
            inputHover: false,
            inputValue: null,
            presentText: null,
            presentTags: [],
            checkedNodes: [],
            filtering: false,
            suggestions: [],
            inputInitialHeight: 0,
            pressDeleteCount: 0,
        };
    },

    computed: {
        realSize() {
            const _zxFormItemSize = (this.zxFormItem || {}).zxFormItemSize;
            return this.size || _zxFormItemSize || (this.$ELEMENT || {}).size;
        },
        tagSize() {
            return ['small', 'mini'].indexOf(this.realSize) > -1
                ? 'mini'
                : 'small';
        },
        isDisabled() {
            return this.disabled || (this.zxForm || {}).disabled;
        },
        config() {
            const config = this.props || {};
            const { $attrs } = this;

            Object
                .keys(MigratingProps)
                .forEach(oldProp => {
                    const { newProp, type } = MigratingProps[oldProp];
                    let oldValue = $attrs[oldProp] || $attrs[kebabCase(oldProp)];
                    if (isDef(oldProp) && !isDef(config[newProp])) {
                        if (type === Boolean && oldValue === '') {
                            oldValue = true;
                        }
                        config[newProp] = oldValue;
                    }
                });

            return config;
        },
        multiple() {
            return this.config.multiple;
        },
        leafOnly() {
            return !this.config.checkStrictly;
        },
        readonly() {
            return !this.filterable || this.multiple;
        },
        clearBtnVisible() {
            if (!this.clearable || this.isDisabled || this.filtering || !this.inputHover) {
                return false;
            }

            return this.multiple
                ? !!this.checkedNodes.filter(node => !node.isDisabled).length
                : !!this.presentText;
        },
        panel() {
            return this.$refs.panel;
        },
    },

    watch: {
        disabled() {
            this.computePresentContent();
        },
        value(val) {
            if (!isEqual(val, this.checkedValue)) {
                this.checkedValue = val;
                this.computePresentContent();
            }
        },
        checkedValue(val) {
            const { value, dropDownVisible } = this;
            const { checkStrictly, multiple } = this.config;

            if (!isEqual(val, value) || isUndefined(value)) {
                this.computePresentContent();
                // hide dropdown when single mode
                if (!multiple && !checkStrictly && dropDownVisible) {
                    this.toggleDropDownVisible(false);
                }

                this.$emit('input', val);
                this.$emit('change', val);
                this.dispatch('ZxFormItem', 'el.form.change', [val]);
            }
        },
        options: {
            handler: function () {
                this.$nextTick(this.computePresentContent);
            },
            deep: true,
        },
        presentText(val) {
            this.inputValue = val;
        },
        presentTags(val, oldVal) {
            if (this.multiple && (val.length || oldVal.length)) {
                this.$nextTick(this.updateStyle);
            }
        },
        filtering(val) {
            this.$nextTick(this.updatePopper);
        },
    },

    mounted() {
        const { input } = this.$refs;
        if (input && input.$el) {
            this.inputInitialHeight = input.$el.offsetHeight || InputSizeMap[this.realSize] || 40;
        }

        if (!isEmpty(this.value)) {
            this.computePresentContent();
        }

        this.filterHandler = debounce(this.debounce, () => {
            const { inputValue } = this;

            if (!inputValue) {
                this.filtering = false;
                return;
            }

            const before = this.beforeFilter(inputValue);
            if (before && before.then) {
                before.then(this.getSuggestions);
            } else if (before !== false) {
                this.getSuggestions();
            } else {
                this.filtering = false;
            }
        });

        addResizeListener(this.$el, this.updateStyle);
    },

    beforeDestroy() {
        removeResizeListener(this.$el, this.updateStyle);
    },

    methods: {
        getMigratingConfig() {
            return {
                props: {
                    'expand-trigger': 'expand-trigger is removed, use `props.expandTrigger` instead.',
                    'change-on-select': 'change-on-select is removed, use `props.checkStrictly` instead.',
                    'hover-threshold': 'hover-threshold is removed, use `props.hoverThreshold` instead',
                },
                events: {
                    'active-item-change': 'active-item-change is renamed to expand-change',
                },
            };
        },
        toggleDropDownVisible(visible) {
            if (this.isDisabled) return;

            const { dropDownVisible } = this;
            const { input } = this.$refs;
            visible = isDef(visible) ? visible : !dropDownVisible;
            if (visible !== dropDownVisible) {
                this.dropDownVisible = visible;
                if (visible) {
                    this.$nextTick(() => {
                        this.updatePopper();
                        this.panel.scrollIntoView();
                    });
                }
                input.$refs.input.setAttribute('aria-expanded', visible);
                this.$emit('visible-change', visible);
            }
        },
        handleDropdownLeave() {
            this.filtering = false;
            this.inputValue = this.presentText;
        },
        handleKeyDown(event) {
            switch (event.keyCode) {
                case KeyCode.enter:
                    this.toggleDropDownVisible();
                    break;
                case KeyCode.down:
                    this.toggleDropDownVisible(true);
                    this.focusFirstNode();
                    event.preventDefault();
                    break;
                case KeyCode.esc:
                case KeyCode.tab:
                    this.toggleDropDownVisible(false);
                    break;
            }
        },
        handleFocus(e) {
            this.$emit('focus', e);
        },
        handleBlur(e) {
            this.$emit('blur', e);
        },
        handleInput(val, event) {
            !this.dropDownVisible && this.toggleDropDownVisible(true);

            if (event && event.isComposing) return;
            if (val) {
                this.filterHandler();
            } else {
                this.filtering = false;
            }
        },
        handleClear() {
            this.presentText = '';
            this.panel.clearCheckedNodes();
        },
        handleExpandChange(value) {
            this.$nextTick(this.updatePopper.bind(this));
            this.$emit('expand-change', value);
            this.$emit('active-item-change', value); // Deprecated
        },
        focusFirstNode() {
            this.$nextTick(() => {
                const { filtering } = this;
                const { popper, suggestionPanel } = this.$refs;
                let firstNode = null;

                if (filtering && suggestionPanel) {
                    firstNode = suggestionPanel.$el.querySelector('.zx-cascader__suggestion-item');
                } else {
                    const firstMenu = popper.querySelector('.zx-cascader-menu');
                    firstNode = firstMenu.querySelector('.zx-cascader-node[tabindex="-1"]');
                }

                if (firstNode) {
                    firstNode.focus();
                    !filtering && firstNode.click();
                }
            });
        },
        computePresentContent() {
            // nextTick is required, because checked nodes may not change right now
            this.$nextTick(() => {
                if (this.config.multiple) {
                    this.computePresentTags();
                    this.presentText = this.presentTags.length ? ' ' : null;
                } else {
                    this.computePresentText();
                }
            });
        },
        computePresentText() {
            const { checkedValue, config } = this;
            if (!isEmpty(checkedValue)) {
                const node = this.panel.getNodeByValue(checkedValue);
                if (node && (config.checkStrictly || node.isLeaf)) {
                    this.presentText = node.getText(this.showAllLevels, this.separator);
                    return;
                }
            }
            this.presentText = null;
        },
        computePresentTags() {
            const { isDisabled, leafOnly, showAllLevels, separator, collapseTags } = this;
            const checkedNodes = this.getCheckedNodes(leafOnly);
            const tags = [];

            const genTag = node => ({
                node,
                key: node.uid,
                text: node.getText(showAllLevels, separator),
                hitState: false,
                closable: !isDisabled && !node.isDisabled,
            });

            if (checkedNodes.length) {
                const [first, ...rest] = checkedNodes;
                const restCount = rest.length;
                tags.push(genTag(first));

                if (restCount) {
                    if (collapseTags) {
                        tags.push({
                            key: -1,
                            text: `+ ${restCount}`,
                            closable: false,
                        });
                    } else {
                        rest.forEach(node => tags.push(genTag(node)));
                    }
                }
            }

            this.checkedNodes = checkedNodes;
            this.presentTags = tags;
        },
        getSuggestions() {
            let { filterMethod } = this;

            if (!isFunction(filterMethod)) {
                filterMethod = (node, keyword) => node.text.includes(keyword);
            }

            const suggestions = this.panel.getFlattedNodes(this.leafOnly)
                .filter(node => {
                    if (node.isDisabled) return false;
                    node.text = node.getText(this.showAllLevels, this.separator) || '';
                    return filterMethod(node, this.inputValue);
                });

            if (this.multiple) {
                this.presentTags.forEach(tag => {
                    tag.hitState = false;
                });
            } else {
                suggestions.forEach(node => {
                    node.checked = isEqual(this.checkedValue, node.getValueByOption());
                });
            }

            this.filtering = true;
            this.suggestions = suggestions;
            this.$nextTick(this.updatePopper);
        },
        handleSuggestionKeyDown(event) {
            const { keyCode, target } = event;
            switch (keyCode) {
                case KeyCode.enter:
                    target.click();
                    break;
                case KeyCode.up:
                    const prev = target.previousElementSibling;
                    prev && prev.focus();
                    break;
                case KeyCode.down:
                    const next = target.nextElementSibling;
                    next && next.focus();
                    break;
                case KeyCode.esc:
                case KeyCode.tab:
                    this.toggleDropDownVisible(false);
                    break;
            }
        },
        handleDelete() {
            const { inputValue, pressDeleteCount, presentTags } = this;
            const lastIndex = presentTags.length - 1;
            const lastTag = presentTags[lastIndex];
            this.pressDeleteCount = inputValue ? 0 : pressDeleteCount + 1;

            if (!lastTag) return;

            if (this.pressDeleteCount) {
                if (lastTag.hitState) {
                    this.deleteTag(lastIndex);
                } else {
                    lastTag.hitState = true;
                }
            }
        },
        handleSuggestionClick(index) {
            const { multiple } = this;
            const targetNode = this.suggestions[index];

            if (multiple) {
                const { checked } = targetNode;
                targetNode.doCheck(!checked);
                this.panel.calculateMultiCheckedValue();
            } else {
                this.checkedValue = targetNode.getValueByOption();
                this.toggleDropDownVisible(false);
            }
        },
        deleteTag(index) {
            const { checkedValue } = this;
            const val = checkedValue[index];
            this.checkedValue = checkedValue.filter((n, i) => i !== index);
            this.$emit('remove-tag', val);
        },
        updateStyle() {
            const { $el, inputInitialHeight } = this;
            if (this.$isServer || !$el) return;

            const { suggestionPanel } = this.$refs;
            const inputInner = $el.querySelector('.zx-input__inner');

            if (!inputInner) return;

            const tags = $el.querySelector('.zx-cascader__tags');
            let suggestionPanelEl = null;

            if (suggestionPanel && (suggestionPanelEl = suggestionPanel.$el)) {
                const suggestionList = suggestionPanelEl.querySelector('.zx-cascader__suggestion-list');
                suggestionList.style.minWidth = inputInner.offsetWidth + 'px';
            }

            if (tags) {
                const { offsetHeight } = tags;
                const height = Math.max(offsetHeight + 6, inputInitialHeight) + 'px';
                inputInner.style.height = height;
                this.updatePopper();
            }
        },

        /**
         * public methods
         */
        getCheckedNodes(leafOnly) {
            return this.panel.getCheckedNodes(leafOnly);
        },
    },
};
</script>
